import fs from 'node:fs/promises';
import path from 'node:path';
import { logger } from '../../core/logger.js';
import { packageJson, findPackagePath, hasProperties } from './utils.js';
import { loadConfig, getConfig } from '../../core/config.js';
import { pathToFileURL } from 'node:url';
import { bold, color } from '../../core/color.js';
import { ALLOWED_EXTENSIONS } from '../../core/constants.js';
import { Mode } from '../../core/mode.js';
import { Compiler } from './compiler.js';
import { IS_BUN_RUNTIME } from './runtime-utils.js';
import { getContextType, getIntegrationType } from './commands.js';

const BASE_MANIFEST = {
  __README: "This file was automatically generated by Robo.js - do not edit manually.",
  __robo: {
    config: null,
    language: "javascript",
    mode: Mode.get(),
    type: "robo"
  },
  api: {},
  commands: {},
  context: {
    message: {},
    user: {}
  },
  events: {},
  permissions: [],
  middleware: [],
  scopes: []
};
const INTENT_PERMISSIONS = {
  DirectMessages: [],
  DirectMessageReactions: [],
  DirectMessageTyping: [],
  Guilds: ["ViewChannel"],
  GuildMembers: [],
  GuildBans: ["BanMembers", "ViewAuditLog"],
  GuildEmojisAndStickers: ["ManageEmojisAndStickers"],
  GuildIntegrations: ["ManageGuild", "ViewAuditLog"],
  GuildWebhooks: ["ManageWebhooks", "ViewAuditLog"],
  GuildInvites: ["CreateInstantInvite", "ManageGuild"],
  GuildVoiceStates: ["Connect", "Speak", "MuteMembers", "DeafenMembers", "MoveMembers", "UseVAD"],
  GuildPresences: [],
  GuildMessages: ["ReadMessageHistory", "SendMessages"],
  GuildMessageReactions: ["ReadMessageHistory", "AddReactions"],
  GuildMessageTyping: ["ReadMessageHistory"]
};
const mergeEvents = (baseEvents, newEvents) => {
  const mergedEvents = { ...baseEvents };
  for (const eventName in newEvents) {
    const baseEventArray = mergedEvents[eventName] || [];
    const newEventArray = newEvents[eventName];
    mergedEvents[eventName] = [...baseEventArray, ...newEventArray];
  }
  return mergedEvents;
};
async function generateManifest(generatedDefaults, type) {
  const config = await loadConfig("robo", true);
  const pluginsManifest = type === "plugin" ? BASE_MANIFEST : await readPluginManifest(config?.plugins);
  const api = await generateEntries("api", []);
  const commands = await generateEntries("commands", Object.keys(generatedDefaults?.commands ?? {}));
  const context = await generateEntries("context", Object.keys(generatedDefaults?.context ?? {}));
  const events = await generateEntries("events", Object.keys(generatedDefaults?.events ?? {}));
  const middleware = Object.values(await generateEntries("middleware", [])).flat();
  const newManifest = {
    ...BASE_MANIFEST,
    ...pluginsManifest,
    __robo: {
      config: redactPluginOptions(config),
      language: Compiler.isTypescriptProject() ? "typescript" : "javascript",
      mode: Mode.get(),
      seed: config.seed ? {
        description: config.seed.description
      } : void 0,
      type,
      updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
      version: packageJson.version
    },
    api: {
      ...pluginsManifest.api,
      ...api
    },
    commands: {
      ...pluginsManifest.commands,
      ...commands
    },
    context: {
      message: {
        ...pluginsManifest.context?.message,
        ...context.message
      },
      user: {
        ...pluginsManifest.context?.user,
        ...context.user
      }
    },
    events: mergeEvents(pluginsManifest.events, events),
    middleware: [...pluginsManifest.middleware, ...middleware]
  };
  newManifest.permissions = await generatePermissions(config);
  newManifest.scopes = generateScopes(config, newManifest);
  newManifest.api = Object.fromEntries(Object.entries(newManifest.api).sort(([a], [b]) => a.localeCompare(b)));
  newManifest.commands = Object.fromEntries(Object.entries(newManifest.commands).sort(([a], [b]) => a.localeCompare(b)));
  newManifest.events = Object.fromEntries(Object.entries(newManifest.events).sort(([a], [b]) => a.localeCompare(b)));
  await fs.mkdir(".robo", { recursive: true });
  await fs.writeFile(path.join(".robo", "manifest.json"), JSON.stringify(newManifest, jsonReplacer, 2));
  logger.debug(`Generated manifest with paths:`, bold(Object.keys(newManifest).join(", ")));
  return newManifest;
}
async function readPluginManifest(plugins) {
  let pluginsManifest = BASE_MANIFEST;
  if (!plugins?.length) {
    return pluginsManifest;
  }
  logger.debug(`Reading plugins...`, plugins);
  for (const plugin of plugins) {
    const pluginName = typeof plugin === "string" ? plugin : plugin[0];
    const packagePath = await findPackagePath(pluginName, process.cwd());
    if (!packagePath) {
      logger.debug(`Plugin ${pluginName} is not installed. Skipping...`);
      continue;
    }
    const manifest = await Compiler.useManifest({
      basePath: packagePath,
      name: pluginName
    });
    logger.debug(`Successfully loaded plugin manifest for ${pluginName} with metadata:`, manifest?.__robo);
    const validPermissions = manifest.permissions && typeof manifest.permissions !== "number";
    pluginsManifest = {
      ...pluginsManifest,
      api: {
        ...pluginsManifest.api,
        ...manifest.api
      },
      commands: {
        ...pluginsManifest.commands,
        ...manifest.commands
      },
      context: {
        message: {
          ...pluginsManifest.context?.message,
          ...manifest.context?.message
        },
        user: {
          ...pluginsManifest.context?.user,
          ...manifest.context?.user
        }
      },
      events: mergeEvents(pluginsManifest.events, manifest.events),
      middleware: [...pluginsManifest.middleware ?? [], ...manifest.middleware ?? []],
      permissions: [
        ...pluginsManifest.permissions,
        ...validPermissions ? manifest.permissions : []
      ],
      scopes: [...pluginsManifest.scopes, ...manifest.scopes ?? []]
    };
  }
  return pluginsManifest;
}
function jsonReplacer(key, value) {
  if (key === "defaultMemberPermissions" && typeof value === "bigint") {
    return value.toString() + "n";
  } else {
    return value;
  }
}
async function generatePermissions(config) {
  const permissions = [];
  const autoPermissions = config?.invite?.autoPermissions ?? true;
  if (autoPermissions) {
    const intents = Object.values(config.clientOptions?.intents || {});
    for (const intent of intents) {
      if (typeof intent === "string") {
        const intentPermissions = INTENT_PERMISSIONS[intent];
        if (intentPermissions) {
          permissions.push(...intentPermissions);
        }
      }
    }
  }
  const configPermissions = config.invite?.permissions;
  if (typeof configPermissions !== "number" && configPermissions?.length) {
    permissions.push(...configPermissions);
  }
  permissions.sort((a, b) => a.localeCompare(b));
  return [...new Set(permissions)].filter((permission) => permission);
}
function generateScopes(config, newManifest) {
  const scopes = ["bot"];
  if (Object.keys(newManifest.commands).length) {
    scopes.push("applications.commands");
  }
  if (config.invite?.scopes?.length) {
    scopes.push(...config.invite.scopes);
  }
  scopes.sort((a, b) => a.localeCompare(b));
  return [...new Set(scopes)].filter((scope) => scope);
}
async function scanDir(predicate, options) {
  const {
    buildDirectory,
    recurseModules = true,
    recursionKeys = [],
    recursionModuleKeys = [],
    recursionPath,
    type
  } = options;
  let directoryPath = recursionPath;
  if (!directoryPath) {
    const buildDir = buildDirectory ? path.join(process.cwd(), buildDirectory) : path.join(process.cwd(), ".robo", "build");
    directoryPath = path.join(buildDir, type);
  }
  let directory = [];
  try {
    directory = await fs.readdir(directoryPath);
  } catch (error) {
    if (hasProperties(error, ["code"]) && error.code === "ENOENT") ; else {
      throw error;
    }
  }
  const files = [];
  const directories = [];
  await Promise.all(
    directory.map(async (file) => {
      const fullPath = path.resolve(directoryPath, file);
      const stats = await fs.stat(fullPath);
      if (stats.isFile() && !ALLOWED_EXTENSIONS.includes(path.extname(file))) {
        return;
      } else if (stats.isFile()) {
        files.push(file);
      } else if (stats.isDirectory()) {
        directories.push(file);
      } else {
        logger.debug(stats);
        logger.warn(`Unknown file or directory encountered while scanning directory: ${fullPath}`);
      }
    })
  );
  const modules = [];
  if (recurseModules) {
    try {
      const modulesPath = recursionPath ? path.join(recursionPath, "..", "modules") : path.join(process.cwd(), ".robo", "build", "modules");
      const modulesDirectory = await fs.readdir(modulesPath);
      await Promise.all(
        modulesDirectory.map(async (module) => {
          try {
            const fullPath = path.resolve(modulesPath, module, type);
            const moduleParts = module.split(".");
            const mode = moduleParts.length > 1 ? moduleParts.pop() : Mode.get();
            if (mode && !Mode.is(mode)) {
              logger.debug(`Skipping module ${module} with other mode: ${mode}`);
              return;
            }
            const stats = await fs.stat(fullPath);
            if (stats.isDirectory()) {
              modules.push(path.join(modulesPath, module, type));
            }
          } catch (error) {
            if (hasProperties(error, ["code"]) && !["ENOENT", "ENOTDIR"].includes(error.code)) {
              throw error;
            }
          }
        })
      );
    } catch (error) {
      if (hasProperties(error, ["code"]) && !["ENOENT", "ENOTDIR"].includes(error.code)) {
        throw error;
      }
    }
  }
  await Promise.all(
    files.map(async (file) => {
      const fileKeys = [...recursionKeys, path.basename(file, path.extname(file))];
      const fullPath = path.resolve(directoryPath, file);
      return predicate(fileKeys, fullPath, recursionModuleKeys);
    })
  );
  await Promise.all(
    directories.map(async (childDir) => {
      const nestedPath = path.resolve(directoryPath, childDir);
      const fileKeys = [...recursionKeys, childDir];
      return scanDir(predicate, {
        buildDirectory,
        recursionKeys: fileKeys,
        recurseModules: false,
        recursionModuleKeys,
        recursionPath: nestedPath,
        type
      });
    })
  );
  await Promise.all(
    modules.map(async (module) => {
      const nestedPath = path.resolve(module);
      const moduleKeys = [...recursionModuleKeys, path.basename(path.dirname(module))];
      return scanDir(predicate, {
        buildDirectory,
        recursionKeys: [],
        recurseModules: true,
        recursionModuleKeys: moduleKeys,
        recursionPath: nestedPath,
        type
      });
    })
  );
}
async function generateEntries(type, generatedKeys) {
  try {
    const entries = {};
    await scanDir(
      async (fileKeys, fullPath, moduleKeys) => {
        logger.debug(`[${type}] Generating`, fileKeys, "from", fullPath);
        const isGenerated = generatedKeys.includes(fileKeys.join("/"));
        let importPath = pathToFileURL(fullPath).toString();
        if (IS_BUN_RUNTIME) {
          importPath = decodeURIComponent(importPath);
        }
        const module = await import(importPath);
        let entry = {
          ...getValue(type, module.config),
          __auto: isGenerated ? true : void 0,
          __module: moduleKeys.join("/") || void 0,
          __path: fullPath.replace(process.cwd(), "")
        };
        let existingEntry = entries[fileKeys[0]];
        entry = Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b)));
        if (type === "events" && !existingEntry) {
          entries[fileKeys[0]] = existingEntry = [];
        }
        if (type === "events" && Array.isArray(existingEntry)) {
          existingEntry.push(entry);
        }
        if (type === "context" && fileKeys.length === 2) {
          const contextType = fileKeys[0];
          if (!entries[contextType]) {
            entries[contextType] = {};
          }
          const entriesRecord = entries[contextType];
          entriesRecord[fileKeys[1]] = entry;
        }
        if (type === "commands" && fileKeys.length === 3) {
          let parentCommand = entries[fileKeys[0]];
          if (parentCommand?.__path) {
            const commandPath = color.bold(`/src/${type}/${parentCommand.__path}`);
            logger.error("You cannot have a parent command alongside subcommand groups! Source: " + commandPath);
            process.exit(1);
          }
          if (!parentCommand) {
            parentCommand = { subcommands: {} };
            entries[fileKeys[0]] = parentCommand;
          }
          let parentSubcommand = parentCommand.subcommands[fileKeys[1]];
          if (parentSubcommand?.__path) {
            const subcommandPath = color.bold(`/src/${type}/${parentSubcommand.__path}`);
            logger.error("You cannot have a subcommand alongside subcommand groups! Source: " + subcommandPath);
            process.exit(1);
          }
          if (!parentSubcommand) {
            parentSubcommand = { subcommands: {} };
            parentCommand.subcommands[fileKeys[1]] = parentSubcommand;
          }
          parentSubcommand.subcommands[fileKeys[2]] = entry;
        }
        if (type === "commands" && fileKeys.length === 2) {
          let parentCommand = entries[fileKeys[0]];
          if (parentCommand?.__path) {
            const commandPath = color.bold(`/src/${type}/${parentCommand.__path}`);
            logger.error("You cannot have a parent command alongside subcommands! Source: " + commandPath);
            process.exit(1);
          }
          if (!parentCommand) {
            parentCommand = { subcommands: {} };
            entries[fileKeys[0]] = parentCommand;
          }
          parentCommand.subcommands[fileKeys[1]] = entry;
        }
        if (type === "commands" && fileKeys.length === 1) {
          entries[fileKeys[0]] = entry;
        }
        if (type === "middleware") {
          entries[fileKeys.join("/")] = entry;
        }
        if (type === "api") {
          if (fileKeys.length > 1) {
            let parent = entries[fileKeys[0]];
            if (!parent) {
              parent = { subroutes: {} };
              entries[fileKeys[0]] = parent;
            }
            if (!parent.subroutes) {
              parent.subroutes = {};
            }
            for (let i = 1; i < fileKeys.length - 1; i++) {
              const key = fileKeys[i];
              if (!parent.subroutes[key]) {
                parent.subroutes[key] = { subroutes: {} };
              }
              parent = parent.subroutes[key];
            }
            parent.subroutes[fileKeys[fileKeys.length - 1]] = entry;
          } else {
            entries[fileKeys[0]] = entry;
          }
        }
      },
      {
        buildDirectory: getConfig().experimental?.buildDirectory,
        type
      }
    );
    logger.debug(`Generated ${Object.keys(entries).length} unique ${type}`);
    return entries;
  } catch (error) {
    if (hasProperties(error, ["code"]) && error.code === "ENOENT") {
      return {};
    }
    throw error;
  }
}
function getValue(type, config) {
  const value = {};
  if (!config) {
    return value;
  }
  if (type === "commands" && config) {
    if (config.contexts !== void 0) {
      value.contexts = config.contexts.map(getContextType);
    }
    if (config.defaultMemberPermissions !== void 0) {
      value.defaultMemberPermissions = config.defaultMemberPermissions;
    }
    if (config.dmPermission !== void 0) {
      value.dmPermission = config.dmPermission;
    }
    if (config.description) {
      value.description = config.description;
    }
    if (config.descriptionLocalizations) {
      value.descriptionLocalizations = config.descriptionLocalizations;
    }
    if (config.integrationTypes) {
      value.integrationTypes = config.integrationTypes.map(getIntegrationType);
    }
    if (config.options) {
      value.options = config.options.map((option) => {
        const optionValue = {
          name: option.name
        };
        if (option.autocomplete) {
          optionValue.autocomplete = option.autocomplete;
        }
        if (option.choices) {
          optionValue.choices = option.choices;
        }
        if (option.description) {
          optionValue.description = option.description;
        }
        if (option.descriptionLocalizations) {
          optionValue.descriptionLocalizations = option.descriptionLocalizations;
        }
        if (option.max) {
          optionValue.max = option.max;
        }
        if (option.min) {
          optionValue.min = option.min;
        }
        if (option.nameLocalizations) {
          optionValue.nameLocalizations = option.nameLocalizations;
        }
        if (option.required) {
          optionValue.required = option.required;
        }
        if (option.type) {
          optionValue.type = option.type;
        }
        return optionValue;
      });
      const options = [...value.options].sort((a, b) => {
        if (a.required && !b.required) {
          return -1;
        }
        if (!a.required && b.required) {
          return 1;
        }
        return 0;
      });
      value.options = options;
      if (config.sage !== void 0) {
        value.sage = config.sage;
      }
    }
  }
  if (type === "context" && config !== void 0) {
    if (config.contexts) {
      value.contexts = config.contexts.map(getContextType);
    }
    if (config.nameLocalizations) {
      value.nameLocalizations = config.nameLocalizations;
    }
    if (config.defaultMemberPermissions) {
      value.defaultMemberPermissions = config.defaultMemberPermissions;
    }
    if (config.dmPermission !== void 0) {
      value.dmPermission = config.dmPermission;
    }
    if (config.integrationTypes) {
      value.integrationTypes = config.integrationTypes.map(getIntegrationType);
    }
  }
  if (type === "events" && config) {
    value.frequency = config.frequency ?? "always";
  }
  if (config.timeout !== void 0) {
    value.timeout = config.timeout;
  }
  return Object.keys(value).sort().reduce((acc, key) => {
    acc[key] = value[key];
    return acc;
  }, {});
}
function redactPluginOptions(config) {
  if (!config.plugins) {
    return config;
  }
  const redactedPlugins = config.plugins?.map((plugin) => {
    if (Array.isArray(plugin)) {
      const [pluginName, pluginOptions] = plugin;
      if (typeof pluginOptions === "object") {
        const redactedObj = {};
        for (const key in pluginOptions) {
          if (Object.prototype.hasOwnProperty.call(pluginOptions, key)) {
            redactedObj[key] = "[REDACTED]";
          }
        }
        return [pluginName, redactedObj];
      }
    }
    return plugin;
  });
  return {
    ...config,
    plugins: redactedPlugins
  };
}

export { BASE_MANIFEST, generateManifest };
